//
// RS-274X display engine
//
// This code simply displays a given file which has to be in the Gerber
// RS-274X format.
// This format is used for e.g. for layouts.
//
// The idea is to have a 'virtual' plotter, which tries to emulate the
// states and parameters of a real plotter as close as possible, thereby
// makeing later extensions easier and keeping it as close as possible to the
// real world.
//
// An image export is also possible, which then can be used to either
// be printed by a graphics program or included in any document.
//
// Finally you can also directly print, although i'd rather recommend to
// do that using graphics import in another program.
//



import java.applet.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;


//
// Main application class. This is the construct, similar to the program
// Matrix... Upps.. Sorry, got carried away. Serious, this is really only
// the other shell of the whole thing, providing a simple GUI and some
// import and export functionalities. The real deal is done in the Plotter
// class which does the real work.
//
public class RS274X extends Applet implements ActionListener, ItemListener {
    private String  in;		// Actual input
    private Plotter pl;		// Our virtual plotter
    private boolean singleStep;	// Flag if we have started doing a single plot

    private Font    f;
    private Button  loadButton;
    private Button  exportButton;
    private Choice  exportChoice;
    private Button  drawButton;
    private Button  stepButton;
    private Choice  scaleChoice;
    private Button  printButton;
    private Button  quitButton;



    //
    // Our constructor doesn't do much... :)
    //
    public RS274X() {
        in         = "";
        pl         = new Plotter();
        singleStep = false;

        pl.setSize(640, 480);
    }



    //
    // Old trick learned from an indian to keep the code overhead of a combined
    // Java applet/application as small as possible. To be honest i didn't
    // learn it from an indian, but started doing it like this myself after
    // writing the same code more than once for Applets and Applications.
    // Ah, and yes, you can easily use this Application as an applet and e.g
    // use it to display a static Gerber file which you initialize in the
    // constructor. Sometimes Java is really nice...
    //
    public void init() {
        f            = new Font("Courier", Font.PLAIN, 12);
        loadButton   = new Button("Load");
        exportButton = new Button("Save");
        exportChoice = new Choice();
        drawButton   = new Button("Draw");
        scaleChoice  = new Choice();
        stepButton   = new Button("Step");
        printButton  = new Button("Print");
        quitButton   = new Button("Quit");

        exportChoice.add("640x480");
        exportChoice.add("800x600");
        exportChoice.add("1024x768");
        exportChoice.add("1280x1024");
        exportChoice.add("1600x1200");
        exportChoice.add("2048x1536");

        scaleChoice.add("0.01");
        scaleChoice.add("0.05");
        scaleChoice.add("0.1");
        scaleChoice.add("0.5");
        scaleChoice.add("1.0");
        scaleChoice.add("2.5");
        scaleChoice.add("5.0");
        scaleChoice.add("10.0");
        scaleChoice.add("25.0");
        scaleChoice.add("50.0");
        scaleChoice.add("100.0");
        scaleChoice.select("1.0");
        pl.setScale(1.0, 1.0);

        setLayout(null);
        setBackground(Color.white);
        setSize(640,480);

        addAbsolute(loadButton, this, f, 10, 450, 60, 20);
        loadButton.addActionListener(this);

        addAbsolute(exportButton, this, f, 90, 450, 60, 20);
        exportButton.addActionListener(this);

        addAbsolute(exportChoice, this, f, 160, 450, 60, 20);

        addAbsolute(drawButton, this, f, 250, 450, 60, 20);
        drawButton.addActionListener(this);

        addAbsolute(scaleChoice, this, f, 330, 450, 60, 20);
        scaleChoice.addItemListener(this);

        addAbsolute(stepButton, this, f, 410, 450, 60, 20);
        stepButton.addActionListener(this);

        addAbsolute(printButton, this, f, 490, 450, 60, 20);
        printButton.addActionListener(this);

        addAbsolute(quitButton, this, f, 570, 450, 60, 20);
        quitButton.addActionListener(this);
    }



    //
    // Uh well, what shall i say... We really don't do any special startup
    // stuff.
    //
    public void start() {
    }



    //
    // And the same applies for the stopping of the applet. We just don't have
    // any useless threads running around all the time.
    //
    public void stop() {
    }



    //
    // Needs to be implemented for ActionListeners. Every buttonclick lands
    // here. It's a pretty simple deal. For every button we call the
    // corresponding method.
    //
    public void actionPerformed(ActionEvent e) {
        Object o = e.getSource();
        String c = e.getActionCommand();

        if (o == loadButton) {
            loadInput();
            repaint();
            singleStep = false;
            return;
        }

        if (o == exportButton) {
            exportImage();
            singleStep = false;
            return;
        }

        if (o == drawButton) {
            drawImage();
            singleStep = false;
            return;
        }

        if (o == stepButton) {
            drawSingleStep();
            return;
        }

        if (o == printButton) {
            printImage();
            singleStep = false;
            return;
        }

        if (o == quitButton) {
            System.exit(0);
            return;
        }
    }



    //
    // Needed for an ItemListener. When the user, that is YOU changes the
    // magnification we have to adjust the Plotter instance and call a
    // repaint() as the current display won't be correct anymore.
    //
    public void itemStateChanged(ItemEvent e) {
        Object o = e.getSource();
        String c = (String)e.getItem();

        if (o == scaleChoice) {
            try {
                Double d = new Double(c);
                double sc = d.doubleValue();

                pl.setScale(sc, sc);
                singleStep = false;
                repaint();
            }
            catch (Exception ex) {};

            return;
        }
    }



    //
    // Small helper function to place compontents without a layout manager.
    // Some people might disagree, but i find it simply easier to do it like
    // that. This way i just know where the components are and how they look
    // like. I had some really frustrating experiences with the GridBaglayout,
    // especially when things get really hairy.
    //
    private void addAbsolute(Component c, Container p, Font f, int x, int y, int w, int h) {
        p.add(c);
        c.setFont(f);
        c.setSize(w,h);
        c.setLocation(x,y);
    }



    //
    // Simple loader method. Pops up a small file dialog and the reads the
    // given file. Btw: Garbage in -> Garbage out principle holds here, so
    // think twice which file you load. ;)
    //
    private void loadInput() {
        String fn;
        FileDialog fd= new FileDialog((Frame)this.getParent(), "Load RS-274D/X file...", FileDialog.LOAD);

        fd.show();

        fn = fd.getFile();

        if (fn != null)
            readInput(fn);

    }



    //
    // Does the actual reading of the input file. Uses a BufferedReader,
    // rest is simple line by line reading and appending to the input string.
    // I already considered after seeing a buggy example in the manual to
    // automatically append a '*' after every line, but i fixed the parser in
    // the Plotter instead to take care of some 'omissions'.
    //
    private void readInput(String file) {
        try {
            BufferedReader br;
            String         line;

            in = "";	// Erase old input

            System.out.println("Debug: Opening input file "+file);

            br = new BufferedReader(new FileReader(file));

            System.out.println("Debug: Reading input file "+file);

            line = br.readLine();
            while (line != null) {
                in += line;
                line = br.readLine();
            }

            System.out.println("Debug: Closing input file "+file);

            br.close();
        }
        catch (Exception e) {
            System.out.println("Error: Couldn't read input file "+file);
            in = "";
        }

        System.out.println("Debug: Input file read.");
    }



    //
    // The export of the plot is done by renderning the input into an offscreen
    // image which we then read out using a PixelGrabber. The format of the
    // saved image is PNM or PortableAnyMap, which every program i know can
    // at least read (Photoshop, Gimp, XV, various other graphic tools). The
    // sized is fixed for now, but in later versions i might implement a size
    // selection somehow (small medium large?!?). I should also have print a
    // small dialog that the save was complete, but thats really just cosmetic
    // for now and will be done eventually.
    //
    // 4/18/00: I figured that 700k was just too much, so i looked around the
    // web and found a GPL implementation of a PNG enocder which i am happily
    // using now. That reduces the image size to 5k, which is more reasonable,
    // and PNG can be converted with many utilities nowadays.
    //
    // 4/22/00: Added a choice to select the resolution of the output image.
    // This way you can now export a fairly large image which can be used later
    // for printing (as printing pretty much stinks in Java, even in 1.2).
    //
    private void exportImage() {
        try {
            FileOutputStream bw;
            Image osImage;
            String fn;
            FileDialog fd = new FileDialog((Frame)this.getParent(), "Export RS-274D/X as PNG image...", FileDialog.SAVE);

            fd.show();
            fn = fd.getFile();
            if (fn == null)
                return;

            System.out.println("Debug: Creating offscreen image.");

            switch (exportChoice.getSelectedIndex()) {
                case 0:
                    pl.setSize(640, 480);
                    osImage = this.createImage(640, 480);
                    break;

                case 1:
                    pl.setSize(800, 600);
                    osImage = this.createImage(800, 600);
                    break;

                case 2:
                    pl.setSize(1024, 768);
                    osImage = this.createImage(1024, 768);
                    break;

                case 3:
                    pl.setSize(1280, 1024);
                    osImage = this.createImage(1280, 1024);
                    break;

                case 4:
                    pl.setSize(1600, 1200);
                    osImage = this.createImage(1600, 1200);
                    break;

                case 5:
                    pl.setSize(2048, 1536);
                    osImage = this.createImage(2048, 1536);
                    break;

                default:
                    pl.setSize(640, 480);
                    osImage = this.createImage(640,480);
                    break;
            }

            if (osImage == null) {
                System.out.println("Error: Couldn't create offscreen image");
                return;
            }

            pl.setGraphics(osImage.getGraphics());

            if (pl.plotInput(in) == false)
                System.out.println("Error: Plotter unable to plot input.");
            else
                System.out.println("Debug: Plotting successful.");

            PngEncoder p = new PngEncoder(osImage);
            p.setCompressionLevel(5);

            byte[] pngbytes = p.pngEncode();

            System.out.println("Debug: Opening output file "+fn+".png");

            bw = new FileOutputStream(fn+".png");
            bw.write(pngbytes);

            System.out.println("Debug: Closing output file "+fn+".png");
            bw.close();

        }
        catch (Exception e) {
            System.out.println("Error: Couldn't write output file.");
            System.exit(1);
        }

        System.out.println("Debug: Output file successfully written.");

        // Change back our screen setting :)
        pl.setSize(640, 480);
    }



    //
    // Thats the whole story for displaying the image. Yep, no more magic...
    // Honestly, most of the work is done in the Plotter class, so better
    // look there if you are interested in how the actual plotting of single
    // elements is done.
    //
    private void drawImage() {
        System.out.println("Debug: Start plotting.");

        pl.setGraphics(this.getGraphics());

        if (pl.plotInput(in) == false)
            System.out.println("Error: Plotter unable to plot input.");
        else
            System.out.println("Debug: Plotting successful.");
    }



    //
    // Single step works a little different as we first have to let the Plotter
    // class parse the input and then, after initializing the state machine we
    // can call processInput() to really process only one command. This looks
    // ugly, but otherwise we would have to split up the interface to the
    // Plotter completely. And the problem is that the Plotter class just
    // doesn't know if there is a new input or not, the calling class has to
    // decide that and tell it to the Plotter. So as much as i'd like to keep
    // single step and the complete drawing of the Plot similar i think we have
    // to simply stick with that method here. Maybe enlightenment will strike
    // me in the next couple of days, i surely hope so, as i just belive there
    // has to be a 'cleaner' way to do this.
    //
    private void drawSingleStep() {
        if (singleStep == false) {
            System.out.println("Debug: Start single step.");
            singleStep = true;

            pl.setGraphics(this.getGraphics());
            pl.initStateMachine();
            pl.parseInput(in, 0);
        }


        if (pl.processInput(1) == false)
            System.out.println("Error: Plotter unable to plot input.");
        else
            System.out.println("Debug: Plotting successful.");
    }



    //
    // Printing isn't much different from a draw, only that we do it on a
    // newly created frame and with a PrintJob.
    // Someone might ask why i display the frame and then hide it again, once
    // for getting the printJob and again for printing the components. This is
    // unfortunately due to some buggy JVM's which think that they don't have
    // to print anything if the frame is hidden or which return a null PrintJob
    // for a hidden frame. Belive me, i had to test that for a commercial
    // product i did, i spent quite some time figuring out what happend. I can
    // actually understand not printing anything if the frame is hidden, but
    // the problem with the null PrintJob still puzzles me today...
    // These bugs might all have been fixed in newer JVM's, like the IBM JDK118
    // which i am using right now on Linux for development, but just to be on
    // the safe side i keep it that way.
    //
    private void printImage() {
        System.out.println("Debug: Start printing.");

        Frame frame = new Frame("PrintJob");    // New Frame
        frame.setLayout(null);                  // No Layout...

        frame.setVisible(true);                 // Quickly show the frame
        PrintJob printJob = frame.getToolkit().getPrintJob(frame, "PrintJob", null);
        frame.setVisible(false);                // to get the printjob

        if (printJob == null) {
            frame.dispose();
            System.out.println("Error: Couldn't get printjob");
            return;
        }

        Dimension dimension = printJob.getPageDimension();

        double page_res = (double) printJob.getPageResolution();
        double screen_res = (double)            Toolkit.getDefaultToolkit().getScreenResolution();
        double res_ratio = screen_res / page_res;

        int width = (int) dimension.width;
        int height = (int) dimension.height;

        printJob.lastPageFirst();
        Graphics g = printJob.getGraphics();

        if (g == null) {
            frame.dispose();
            System.out.println("Error: Couldn't get graphics context for printjob");
            return;
        }

        pl.setGraphics(g);

        if (pl.plotInput(in) == false)
            System.out.println("Error: Couldn't render input.");
        else
            System.out.println("Debug: Printing successfull.");

        frame.setBounds(0, 0, width, height);
        frame.setVisible(true);
        frame.printComponents(g);
        frame.setVisible(false);

        g.dispose();
        printJob.end();
        frame.dispose();
    }



    //
    // Uhhh, ahhh, most complicated. No more comments.
    //
    public static void usage() {
        System.out.println("Usage: [jre | java] RS274X [filename]");
    }



    //
    // Refering back to the cool trick again at the beginning with the init()
    // method here the conclusion: I am using the main method to create an
    // instance of the Applet and then call the init() myself in it, just like
    // a web browser would work. And in the end i even call the start() method,
    // again just like a browser. So i kinda simulate the browser call
    // procedure here in the main method. The rest vanilla standard code.
    //
    public static void main(String[] argv) {
        Frame f;   
        RS274X a;

        if (argv.length > 1) {
            usage();
            return;
        }

        f = new Frame("A RS-274X display program");
        a = new RS274X();

        if (argv.length == 1) {
            a.readInput(argv[0]);
        }

        a.init();

        f.add(a);
        f.pack();
        f.show();

        a.start();
    }
};	// <- remains of good old C++ habbits... Bad bad me ;)
